Fork me on GitHub

HandlerMapping

注意:所有文章除特别说明外,转载请注明出处.

第12章 HandlerMapping

[TOC]

HandlerMapping的作用是通过request查找Handler和Interceptors。HandlerMapping包括继承AbstractUrlHandlerMapping和继承AbstractHandlerMapping,这两支都继承抽象类AbstractHandlerMapping。

12.1 AbstractHandlerMapping

AbstractHandlerMapping是HandlerMapping抽象实现。其采用模板模式设计了HandlerMapping实现的整体结构。子类通过模板方法提供初始值或者具体算法即可。这种模式是:首先使用一个抽象实现采用模板模式进行整体设计,然后在子类通过实现模板方法具体实现业务。

HandlerMapping的作用是是通过request查找Handler和Interceptors。获取Handler的过程通过模板方法getHandlerInternal()交给子类。AbstractHandlerMapping中保存了所有的配置Interceptor,在获取Handler后会自己根据从request中获取的lookupPath将相应的Interceptor装配。子类也可以如此。

12.1.1 创建AbstractHandlerMapping

AbstractHandlerMapping继承WebApplicationObjectSupport,初始化时会自动调用模板方法initApplicationContext()方法。AbstractHandlerMapping创建是在initApplicationContext()方法中。

package org.springframework.web.servlet.handler;
public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport implements HandlerMapping, Ordered {
    // ...
 }

protected void initApplicationContext() throws BeansException {
    //该方法是模板方法,给子类提供一个添加或修改Interceptors的入口
    extendInterceptors(this.interceptors);

    //将SpringMVC容器和父容器中所有的MappedInterceptor类型的Bean添加到mappedInterceptors属性
    detectMappedInterceptors(this.mappedInterceptors);

    //该方法的作用是初始化Interceptor
    initInterceptors();
}

//提供给子类扩展拦截器(没有使用)
protected void extendInterceptors(List<Object> interceptors) {

    }

//扫描应用下的MappedInterceptor,并添加到mappedInterceptors
protected void detectMappedInterceptors(List<MappedInterceptor> mappedInterceptors) {
     mappedInterceptors.addAll(
             BeanFactoryUtils.beansOfTypeIncludingAncestors(
                     getApplicationContext(),MappedInterceptor.class, true, false).values());
 }

//归集MappedInterceptor,并适配HandlerInterceptor和WebRequestInterceptor
protected void initInterceptors() {
     if (!this.interceptors.isEmpty()) {
         for (int i = 0; i < this.interceptors.size(); i++) {
             Object interceptor = this.interceptors.get(i);
             if (interceptor == null) {
                 throw new IllegalArgumentException("Entry number " + i + " in interceptors array is null");
             }
             if (interceptor instanceof MappedInterceptor) {
                 mappedInterceptors.add((MappedInterceptor) interceptor);
             }
             else {
                 adaptedInterceptors.add(adaptInterceptor(interceptor));
             }
         }
     }
 }

AbstractHandlerMapping的属性:

    // order赋了最大值,优先级是最小的
    private int order = Integer.MAX_VALUE;

    // 默认的Handler,这边使用的Obejct,子类实现的时候,使用HandlerMethod,HandlerExecutionChain等
    private Object defaultHandler;

    // url计算的辅助类
    private UrlPathHelper urlPathHelper = new UrlPathHelper();

    // 基于ant进行path匹配,解决如/books/{id}场景
    private PathMatcher pathMatcher = new AntPathMatcher();

    // 拦截器配置:两种配置方式 interceptors只用于配置
    // 1 HandlerMapping属性设置;
    // 2 extendInterceptors设置
    private final List<Object> interceptors = new ArrayList<Object>();

    // 从interceptors中解析得到,直接添加给全部handler
    private final List<HandlerInterceptor> adaptedInterceptors = new ArrayList<HandlerInterceptor>();

    // 使用前需要跟url进行匹配,匹配通过才会使用。匹配成功后将其添加到getHandler的返回值HandlerExecutionChain中。
    private final List<MappedInterceptor> mappedInterceptors = new ArrayList<MappedInterceptor>();

提示:WebApplicationObjectSupport用于提供上下文ApplicationContext和ServletContext。AbstractHandlerMapping的创建就是上面三个Interceptor的初始化。

12.1.2 AbstractHandlerMapping的使用

HandlerMapping通过getHandler()方法获取处理器Handler和拦截器Interceptors。

public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    //留给子类具体实现,子类主要做的事情
    Object handler = getHandlerInternal(request);

    //没有获取到则使用默认的Handler
    if (handler == null) {
        handler = getDefaultHandler();
    }
    if (handler == null) {
        return null;
    }
    // Bean name or resolved handler?
    if (handler instanceof String) {
        String handlerName = (String) handler;
        handler = getApplicationContext().getBean(handlerName);
    }
    return getHandlerExecutionChain(handler, request);
}

//这里预留getHandlerInternal(HttpServletRequest request)方法给子类实现
protected abstract Object getHandlerInternal(HttpServletRequest request) throws Exception;

然后封装拦截器到HandlerExecutionChain(),该方法用于添加拦截器。

protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {

    //1.首先使用handler创建HandlerExecutionChain类型变量
    HandlerExecutionChain chain = 
    (handler instanceof HandlerExecutionChain) ? (HandlerExecutionChain) handler : new HandlerExecutionChain(handler);

    //adaptedInterceptors直接添加
    chain.addInterceptors(getAdaptedInterceptors());

    //mappedInterceptors需要根据url匹配通过后添加
    String lookupPath = urlPathHelper.getLookupPathForRequest(request);
    for (MappedInterceptor mappedInterceptor : mappedInterceptors) {
        if (mappedInterceptor.matches(lookupPath, pathMatcher)) {
            chain.addInterceptor(mappedInterceptor.getInterceptor());
        }
    }

    return chain;
}

12.2 AbstractUrlHandlerMapping

从名字可以看出是通过url进行匹配的。大致原理是将url与对应的handler保存到一个Map中,在getHandlerInternal()方法中使用url从Map中获取Handler,AbstractUrlHandlerMapping中实现了具体用url从Map中获取Handler的过程。

@Override
protected Object getHandlerInternal(HttpServletRequest request) throws Exception {
    // 根据request获取url
    String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
    // 根据url查找handler
    Object handler = lookupHandler(lookupPath, request);
    if (handler == null) {
        // 如果没有匹配到handler需要查找默认的,下面需要将PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE缓存到request
        //临时变量,保存找到的原始Handler
        Object rawHandler = null;
        if ("/".equals(lookupPath)) {
            rawHandler = getRootHandler();
        }
        if (rawHandler == null) {
            rawHandler = getDefaultHandler();
        }
        if (rawHandler != null) {
            //如果是String类型则到容器中查找具体的bean
            if (rawHandler instanceof String) {
                String handlerName = (String) rawHandler;
                rawHandler = getApplicationContext().getBean(handlerName);
            }
            // 预留的校验handler模板方法,没有使用
            validateHandler(rawHandler, request);
            // 添加expose属性到request的拦截器
            handler = buildPathExposingHandler(rawHandler, lookupPath, lookupPath, null);
        }
    }
    if (handler != null && logger.isDebugEnabled()) {
        logger.debug("Mapping [" + lookupPath + "] to " + handler);
    }
    else if (handler == null && logger.isTraceEnabled()) {
        logger.trace("No handler mapping found for [" + lookupPath + "]");
    }
    return handler;
}

提示:这里的lookupHandler()方法用于使用lookupPath从Map中查找Handler。buildPathExposiongHandler()方法用于给查找到的Handler注册两个拦截器PathExposingHandlerIntercptor和UriTemplateVariablesHandlerInterceptor。这两个是内部拦截器,主要作用是将与当前url实际匹配的pattern、匹配条件和url模板参数等设置到request的属性中。这样一来在后面的处理过程中就能够直接从request中获取。

protected Object lookupHandler(String urlPath, HttpServletRequest request) throws Exception {
    // 直接根据url进行查找handler,直接从Map中获取
    Object handler = this.handlerMap.get(urlPath);
    if (handler != null) {
        // 如果是String类型直接从容器中获取
        if (handler instanceof String) {
            String handlerName = (String) handler;
            handler = getApplicationContext().getBean(handlerName);
        }
        validateHandler(handler, request);
        return buildPathExposingHandler(handler, urlPath, urlPath, null);
    }
    // Pattern match? 通过表达式进行匹配具体通过AntPathMatcher实现,具体后面分析
    List<String> matchingPatterns = new ArrayList<String>();
    for (String registeredPattern : this.handlerMap.keySet()) {
        if (getPathMatcher().match(registeredPattern, urlPath)) {
            matchingPatterns.add(registeredPattern);
        }
    }
    String bestPatternMatch = null;
    Comparator<String> patternComparator = getPathMatcher().getPatternComparator(urlPath);
    if (!matchingPatterns.isEmpty()) {
        Collections.sort(matchingPatterns, patternComparator);
        if (logger.isDebugEnabled()) {
            logger.debug("Matching patterns for request [" + urlPath + "] are " + matchingPatterns);
        }
        // order序号最小的优先级最高
        bestPatternMatch = matchingPatterns.get(0);
    }
    if (bestPatternMatch != null) {
        handler = this.handlerMap.get(bestPatternMatch);
        // 如果是String类型则从容器中获取
        if (handler instanceof String) {
            String handlerName = (String) handler;
            handler = getApplicationContext().getBean(handlerName);
        }
        validateHandler(handler, request);
        String pathWithinMapping = getPathMatcher().extractPathWithinPattern(bestPatternMatch, urlPath);

        //处理使用sort()方法排序之后,多个Pattern顺序相同,返回值为0的情况
        Map<String, String> uriTemplateVariables = new LinkedHashMap<String, String>();
        for (String matchingPattern : matchingPatterns) {
            if (patternComparator.compare(bestPatternMatch, matchingPattern) == 0) {
                Map<String, String> vars = getPathMatcher().extractUriTemplateVariables(matchingPattern, urlPath);
                Map<String, String> decodedVars = getUrlPathHelper().decodePathVariables(request, vars);
                uriTemplateVariables.putAll(decodedVars);
            }
        }
        if (logger.isDebugEnabled()) {
            logger.debug("URI Template variables for request [" + urlPath + "] are " + uriTemplateVariables);
        }
        return buildPathExposingHandler(handler, bestPatternMatch, pathWithinMapping, uriTemplateVariables);
    }
    // No handler found...
    return null;
}

12.2.1 SimpleUrlHandlerMapping

该类定义了一个Map变量(两个作用:1.方便配置。2.在注册前做一些预处理。如:确保所有的url都以””/“开头),将所有的url和handler的对应关系放在里面,最后注册到父类的Map中。AbstractDetectingUrlHandlerMapping是将容器中所有bean拿出来,按照一定规则注册到父类的Map中。

SimpleUrlHandlerMapping在创建时重写父类的initApplicationContext()方法,调用registerHandlers()方法完成Handler注册。registerHandlers()方法调用AbstractHandlerMapping的registerHandler()方法将配置的urlMap注册到AbstractUrlHandlerMapping的Map中。

总结:该类就是直接将配置的内容注册到AbstractUrlHandlerMapping中去。

12.2.2 AbstractDetectingUrlHandlerMapping

该类也是通过重写initApplicationContext来注册Handler,在里面调用了detectHandler()方法。在detectHandlers()中根据配置的detectHand-lersInAncestorContexts参数从Springmvc容器或者Springmvc及其父容器中找到所有的bean的beanName,然后使用方法determineUrlsHandler()方法对每个beanName解析出对应的urls。

@Override
public void initApplicationContext() throws ApplicationContextException {
    //进行初始化
    super.initApplicationContext();

    detectHandlers();
}

protected void detectHandlers() throws BeansException {
    if (logger.isDebugEnabled()) {
        logger.debug("Looking for URL mappings in application context: " + getApplicationContext());
    }

    //获取容器的所有bean的名字
    String[] beanNames = (this.detectHandlersInAncestorContexts ?
            BeanFactoryUtils.beanNamesForTypeIncludingAncestors(getApplicationContext(), Object.class) :
            getApplicationContext().getBeanNamesForType(Object.class));

    // 对每个beanName解析url,如果能解析到就注册到父类的Map中
    for (String beanName : beanNames) {

        //使用beanName解析url,是模板方法,子类具体实现
        String[] urls = determineUrlsForHandler(beanName);

        //如果能解析到url则注册到父类
        if (!ObjectUtils.isEmpty(urls)) {
            // 父类的registerHandler方法
            registerHandler(urls, beanName);
        } else {
            if (logger.isDebugEnabled()) {
                logger.debug("Rejected bean name '" + beanName + "': no URL paths identified");
            }
        }
    }
}

//预留的模板方法
protected abstract String[] determineUrlsForHandler(String beanName);

提示:AbstractDetectingUrlHandlerMapping中有三个类:BeanNameUrlHandlerMapping | DefaultAnnotationHandlerMapping(@Deprecated 已被弃用) | AbstractControllerUrlHandlerMapping 。

1.BeanNameUrlHandlerMapping

该类是检查beanName和alias是否是以”/“开头,如果是则将其作为url,里面只有一个determineUrlsForHandler()方法。

/**
 * Checks name and aliases of the given bean for URLs, starting with "/".
 */
@Override
protected String[] determineUrlsForHandler(String beanName) {
    List<String> urls = new ArrayList<String>();
    if (beanName.startsWith("/")) {
        urls.add(beanName);
    }
    String[] aliases = getApplicationContext().getAliases(beanName);
    for (String alias : aliases) {
        if (alias.startsWith("/")) {
            urls.add(alias);
        }
    }
    return StringUtils.toStringArray(urls);
}

2.AbstractControllerUrlHandlerMapping

该类是将实现了Controller接口或注释@Controller的bean作为Handler,同时可以设置excludedClasses和excludedPackages将不包含的bean或者不包含的包下的所有bean排除在外。这里的determineUrlsForHandler()方法主要负责将符合条件的Handler找出来。

//determineUrlsForHandler()方法主要负责将符合条件的Handler找出来
@Override
protected String[] determineUrlsForHandler(String beanName) {
    Class beanClass = getApplicationContext().getType(beanName);
    if (isEligibleForMapping(beanName, beanClass)) {
        return buildUrlsForHandler(beanName, beanClass);
    }
    else {
        return null;
    }
}

//判断controller是否被排除在外(通过包package排除或类class排除)
protected boolean isEligibleForMapping(String beanName, Class beanClass) {
    if (beanClass == null) {
        if (logger.isDebugEnabled()) {
            logger.debug("Excluding controller bean '" + beanName + "' from class name mapping " +
                    "because its bean type could not be determined");
        }
        return false;
    }

    //排除excludedClasses里配置的类
    if (this.excludedClasses.contains(beanClass)) {
        if (logger.isDebugEnabled()) {
            logger.debug("Excluding controller bean '" + beanName + "' from class name mapping " +
                    "because its bean class is explicitly excluded: " + beanClass.getName());
        }
        return false;
    }
    String beanClassName = beanClass.getName();

    //排除excludedPackages里配置的包下的类
    for (String packageName : this.excludedPackages) {
        if (beanClassName.startsWith(packageName)) {
            if (logger.isDebugEnabled()) {
                logger.debug("Excluding controller bean '" + beanName + "' from class name mapping " +
                        "because its bean class is defined in an excluded package: " + beanClass.getName());
            }
            return false;
        }
    }

    //检查是否实现了Controller接口或注释了@Controller
    return isControllerType(beanClass);
}

2.1 AbstractControllerUrlHandlerMapping 的实现类 ControllerBeanNameUrlHandlerMapping

此类是根据beanName来生产url。

@Override
protected String[] buildUrlsForHandler(String beanName, Class beanClass) {
    List<String> urls = new ArrayList<String>();
    urls.add(generatePathMapping(beanName));
    String[] aliases = getApplicationContext().getAliases(beanName);// 也获取配置的别名
    for (String alias : aliases) {
        urls.add(generatePathMapping(alias));
    }
    return StringUtils.toStringArray(urls);
}

2.2 AbstractControllerUrlHandlerMapping 的实现类 ControllerClassNameUrlHandlerMapping

此类是根据className来生产url。

//对path添加前后缀,还有/
protected String generatePathMapping(String beanName) {
    String name = (beanName.startsWith("/") ? beanName : "/" + beanName);
    StringBuilder path = new StringBuilder();
    if (!name.startsWith(this.urlPrefix)) {
        path.append(this.urlPrefix);
    }
    path.append(name);
    if (!name.endsWith(this.urlSuffix)) {
        path.append(this.urlSuffix);
    }
    return path.toString();
}

提示:ControllerClassNameUrlHandlerMapping直接委托给 generatePathMappings 实现。

@Override
protected String[] buildUrlsForHandler(String beanName, Class beanClass) {
    return generatePathMappings(beanClass);
}


protected String[] generatePathMappings(Class beanClass) {
    StringBuilder pathMapping = buildPathPrefix(beanClass);
    String className = ClassUtils.getShortName(beanClass);
    String path = (className.endsWith(CONTROLLER_SUFFIX) ?
            className.substring(0, className.lastIndexOf(CONTROLLER_SUFFIX)) : className);
    if (path.length() > 0) {
        if (this.caseSensitive) {
            pathMapping.append(path.substring(0, 1).toLowerCase()).append(path.substring(1));
        }
        else {
            pathMapping.append(path.toLowerCase());
        }
    }
    if (isMultiActionControllerType(beanClass)) {
        return new String[] {pathMapping.toString(), pathMapping.toString() + "/*"};
    }
    else {
        return new String[] {pathMapping.toString() + "*"};
    }
}


private StringBuilder buildPathPrefix(Class beanClass) {
    StringBuilder pathMapping = new StringBuilder();
    if (this.pathPrefix != null) {
        pathMapping.append(this.pathPrefix);
        pathMapping.append("/");
    }
    else {
        pathMapping.append("/");
    }
    if (this.basePackage != null) {
        String packageName = ClassUtils.getPackageName(beanClass);
        if (packageName.startsWith(this.basePackage)) {
            String subPackage = packageName.substring(this.basePackage.length()).replace('.', '/');
            pathMapping.append(this.caseSensitive ? subPackage : subPackage.toLowerCase());
            pathMapping.append("/");
        }
    }
    return pathMapping;
}


protected boolean isMultiActionControllerType(Class beanClass) {
    return this.predicate.isMultiActionControllerType(beanClass);
}

12.3 AbstractHandlerMethodMapping

AbstractHandlerMethodMapping系列只有是三个类:AbstractHandlerMethodMapping | RequestMappingInfoHandlerMapping | RequestMappingHandlerMapping。这一系列是将Method作为Handler来使用的。比如经常使用的@RequestMapping所注释的方法就是这种Handler,它专门有一个类型:HandlerMethod,即Method类型的Handler。

12.3.1 创建AbstractHandlerMethodMapping

该类的作用是定义整个算法流程。这里最重要的理解三个Map的含义:

//该map的作用是保存着匹配条件(RequestCondition)和Handler Method的对应关系。
private final Map<T,HandlerMethod> handlerMethods = new LinkedHashMap<T,HandlerMethod>();

//该map的作用是保存着url与匹配条件的对应关系。
private final MultiValueMap<String,T> urlMap = new LinkedMultiValueMap<String,T>();

//该map的作用是保存着name与handlermathod的对应关系。
private final MultiValueMap<String,HandlerMethod> nameMap = new LinkedMultiValueMap<String,HandlerMethod>();

这里的AbstractHandlerMethodMapping实现了initialziingBean接口,所有Spring容器会自动调用其afterProperties方法和afterPropertiesSet又交给initHandlerMethods()方法完成初始化。

protected void initHandlerMethods() {
    if (logger.isDebugEnabled()) {
        logger.debug("Looking for request mappings in application context: " + getApplicationContext());
    }
    String[] beanNames = (this.detectHandlerMethodsInAncestorContexts ?
            BeanFactoryUtils.beanNamesForTypeIncludingAncestors(obtainApplicationContext(), Object.class) :
            obtainApplicationContext().getBeanNamesForType(Object.class));

    for (String beanName : beanNames) {
        if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
            Class<?> beanType = null;
            try {
                beanType = obtainApplicationContext().getType(beanName);
            }
            catch (Throwable ex) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Could not resolve target class for bean with name '" + beanName + "'", ex);
                }
            }
            if (beanType != null && isHandler(beanType)) {
                detectHandlerMethods(beanName);
            }
        }
    }
    handlerMethodsInitialized(getHandlerMethods());
}

提示:1.首先拿到容器里面的所有bean。2.然后根据一定的规则筛选出Handler。3.最后保存到Map中。这里的筛选方法是isHandler()方法,是一个模板方法。具体的筛选是在子类里,筛选的逻辑是检查类前是否有@Controller或者@RequestMapping注解。4.在detectHandlerMethods中,首先从传入的处理器中找到符合要求的方法,然后用registerHandlerMethod进行注册(也就是保存在Map中),从这里可以看出Spring其实是将处理请求的方法所在的类看作处理器,而不是处理请求的方法,不过许多地方需要将请求的方法作为处理器来理解。从handler里获取可以处理请求的method的方法使用。

12.3.1 续 AbstractHandlerMethodMapping 系列之用

这里的主要功能是通过 getHandlerInternal() 方法获取处理器。这里的 getHandlerInternal() 方法做三件事:1.根据request获取ookupPath(url)。2.使用lookupPath和request找handlerMethod。3.如果可以找到handlerMethod则调用它的createWithResolverBean()方法创建新的 HandlerMethod 并返回。这里的 createWithResolverBean() 方法是判断 handlerMethod 里的handler是否是String类型。如果是则改为将其作为 beanName从容器中所取到的bean,但是HandlerMethod中的属性是final类型,不能修改。

12.3.2 RequestMappingInfoHandlerMapping

提供匹配条件RequestMappingInfo的解析处理。

/**
 * 获取url集合,即@RequestMapping中设置的value或path
 */
@Override
protected Set<String> getMappingPathPatterns(RequestMappingInfo info) {
    return info.getPatternsCondition().getPatterns();
}

12.3.3 RequestMappingHandlerMapping

根据@RequestMapping注解生成RequestMappingInfo,同时提供isHandler()方法实现。

/**
 * 使用方法和类型注解@RequestMapping创建RequestMappingInfo对象
 */
@Override
@Nullable
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
    // 创建方法的RequestMappingInfo
    RequestMappingInfo info = createRequestMappingInfo(method);
    if (info != null) {
        // 创建类的RequestMappingInfo
        RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
        if (typeInfo != null) {
            // 将方法RequestMappingInfo和类RequestMappingInfo合并,比如Controller类上有@RequestMapping("/demo"),方法的@RequestMapping("/demo1"),结果为"/demo/demo1"
            info = typeInfo.combine(info);
        }
    }
    return info;
}

@Nullable
private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
    // 获取RequestMapping注解
    RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
    RequestCondition<?> condition = (element instanceof Class ?
            getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element));
    // 调用createRequestMappingInfo创建匹配条件对象
    return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null);
}

/**
 * 构造匹配条件对象
 */
protected RequestMappingInfo createRequestMappingInfo(
        RequestMapping requestMapping, @Nullable RequestCondition<?> customCondition) {

    RequestMappingInfo.Builder builder = RequestMappingInfo
            .paths(resolveEmbeddedValuesInPatterns(requestMapping.path()))
            .methods(requestMapping.method())
            .params(requestMapping.params())
            .headers(requestMapping.headers())
            .consumes(requestMapping.consumes())
            .produces(requestMapping.produces())
            .mappingName(requestMapping.name());
    if (customCondition != null) {
        builder.customCondition(customCondition);
    }
    return builder.options(this.config).build();
}

本文标题:HandlerMapping

文章作者:Bangjin-Hu

发布时间:2019年10月15日 - 09:22:26

最后更新:2020年03月30日 - 08:16:18

原始链接:http://bangjinhu.github.io/undefined/第12章 HandlerMapping/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

Bangjin-Hu wechat
欢迎扫码关注微信公众号,订阅我的微信公众号.
坚持原创技术分享,您的支持是我创作的动力.